local zylib = require("zylib")
local bit = require("bit")
require("pack")
local ex = require("ex")
local rs232 = require("rs232.core")
local socket = require('socket.core');

-- 打开串口
local function open(portName)
	local err, port = rs232.open(portName);
	if err ~= 0 then error(string.format("Failed to open port '%s': %s", portName, rs232.error_tostring(err))); end
	err = port:set_baud_rate(rs232.RS232_BAUD_115200);
	if err ~= 0 then error(string.format("Failed to set baudrate to 115200: %s", rs232.error_tostring(err))); end
	return port;
end

-- 串口 DTR/RTS 状态与 MCU 的 RST/TM 引脚之间的关系：
-- DTR: *OFF(0) / ON(1) ==> RST: *HIGH / LOW
-- RTS: *OFF(0) / ON(1) ==> TM : *LOW  / HIGH
-- RST 控制 MCU 复位: LOW 停机；HIGH 启动
-- TM 控制 MCU 运行模式: =HIGH 烧录模式；=LOW 运行模式

local function pinRST(port, level)
	if level == 0 then
		port:set_dtr(rs232.RS232_DTR_ON);
	else
		port:set_dtr(rs232.RS232_DTR_OFF);
	end
end

local function pinTM(port, level)
	if level == 1 then
		port:set_rts(rs232.RS232_RTS_ON);
	else
		port:set_rts(rs232.RS232_RTS_OFF);
	end
end

-- MCU 重启进入烧录模式
local function resetToFlash(port)
	-- RST=LOW, TM=HIGH
	pinRST(port, 0); pinTM(port, 1); os.sleep(0.5);
	-- RST=HIGH, TM=HIGH
	pinRST(port, 1); pinTM(port, 1);
end

-- MCU 重启进入运行模式
local function resetToRun(port)
	-- RST=LOW, TM=LOW
	pinRST(port, 0); pinTM(port, 0); os.sleep(0.5);
	-- RST=HIGH, TM=LOW
	pinRST(port, 1); pinTM(port, 0);
end

-- 获取当前时间(ms)
local function getTimeMs()
	return socket.gettime() * 1000;
end

-- 字符串是否是有不可显示的(非ASCII可显示字符)二进制数据
local function isBinary(str)
	return str:find("%c") ~= nil
end

-- 发送数据
local function send(port, data)
	local err, len = port:write(data, #data * 10);
	if err ~= 0 then error(string.format("Port write error: %s", rs232.error_tostring(err))); end
	return len;
end

-- 阻塞式接收，条件返回
-- @param timeout 总超时(ms)
-- @param okFlag 正常返回时的标志(字符串)
-- @param okPlain true:使用普通字符串比较; false: 使用模式匹配表达式
-- @param interval 间隔超时(ms)
-- @param limit 最大接收数据大小
-- @return data, reason
local function receive(port, timeout, okFlag, okPlain, failFlag, failPlain, limit, interval)
	local err, ch, len;
	local data = "";
	-- 总超时开始计时时间(ms)
	local beginTime = getTimeMs();
	-- 空闲开始时间(ms)
	local idleTime = nil;

	timeout = timeout or 10000;  		-- 默认超时
	okFlag = okFlag or "#OK>>:"; 		-- 默认正确的响应
	failFlag = failFlag or "#ER>>:";	-- 默认失败的响应
	limit = limit or 10000;      		-- 默认最大接收数据大小

	while true do
		-- 每次读1个字节，超时时间1ms
		err, ch, len = port:read(1, 1);

		if err == rs232.RS232_ERR_TIMEOUT then
			-- 超时，检查总超时和间隔超时
			if timeout and (getTimeMs() - beginTime) >= timeout then return data, "timeout"; end
			if interval and idleTime and (getTimeMs() - idleTime) >= interval then return data, "interval"; end
		elseif err == 0 then
			-- 正常读到数据，把数据添加到缓冲区
			data = data .. ch;
			-- 检查总长度
			if limit and #data >= limit then return data, "limit"; end
			-- 检查 ok 标志
			if okFlag and data:find(okFlag, 1, okPlain) then return data, "ok"; end
			-- 检查 fail 标志
			if failFlag and data:find(failFlag, 1, failPlain) then return data, "fail"; end
			-- 重置空闲开始时间
			idleTime = getTimeMs();
		else
			error(string.format("Port read error: %s", rs232.error_tostring(err)));
		end;
	end
end

-- 发送命令并等待返回
local function cmdAck(port, cmd, timeout, okFlag, okPlain, failFlag, failPlain, limit, interval)
	if isBinary(cmd) then
		print("<< (bin: len = " .. #cmd .. ") ...");
	else
		print("<< " .. cmd);
	end
	
	-- 发送指令
	send(port, cmd);
	
	-- 等待返回
	--return receive(port, timeout, okFlag, okPlain, failFlag, failPlain, limit, interval);
	local ack, reason = receive(port, timeout, okFlag, okPlain, failFlag, failPlain, limit, interval);
	print(">>(" .. reason .. ") " .. ack);
	return ack, reason;
end

-- 根据段的起始地址和段数据的大小，按照 4k 对齐的原则，擦除本段占用的 Flash
--[[
local function eraseSegment(port, start, size)
	-- 待生成的擦除指令
	local cmd;
	
	-- 起始擦除地址: 按 4k 向下对齐
	local eraseStart = bit.band(start, 0xFF000);
	
	-- 要擦除的大小: 按 4k 向上对齐
	local eraseSize = bit.band(start, 0x0FFF) + size;
	if eraseSize <= 4*1024 then
		cmd = string.format("era4k %05X ", eraseStart);
	elseif eraseSize <= 8*1024 then
		cmd = string.format("er8k %05X ", eraseStart);
	elseif eraseSize <= 16*1024 then
		cmd = string.format("er16k %05X ", eraseStart);
	elseif eraseSize <= 32*1024 then
		cmd = string.format("er32k %05X ", eraseStart);
	elseif eraseSize <= 64*1024 then
		cmd = string.format("er64k %05X ", eraseStart);
	elseif eraseSize <= 128*1024 then
		cmd = string.format("er128k %05X ", eraseStart);
	elseif eraseSize <= 256*1024 then
		cmd = string.format("er256k %05X ", eraseStart);
	else
		cmd = "er512 ";
	end

	-- 发送指令
	local ack, reason = cmdAck(port, cmd);
	if reason ~= "ok" then error("error: " .. reason); end
end
--]]

-- 擦除 Flash
local function eraseFlash(port, keepKVStore)
	local cmd, ack, reason;
	-- 默认保留 KVStore
	keepKVStore = zylib.toboolean(keepKVStore, true);
	
	if not keepKVStore then
		-- 全部擦除
		ack, reason = cmdAck(port, "er512 ");
		if reason ~= "ok" then error("error: " .. reason); end
		return;
	end
	
	-- 擦除 0x00000 ~ 0x6FFFF 共 7 个 64k
	-- KVStore 在最后一个64k，保留
	-- 第一个 64k 只能从 0x2000 开始擦除，每次擦除 4k
	for addr = 0x2000, 0xF000, 0x1000 do
		cmd = string.format("era4k %05X ", addr);
		ack, reason = cmdAck(port, cmd);
		if reason ~= "ok" then error("error: " .. reason); end
	end
	for addr = 0x10000, 0x60000, 0x10000 do
		cmd = string.format("er64k %05X ", addr);
		ack, reason = cmdAck(port, cmd);
		if reason ~= "ok" then error("error: " .. reason); end
	end
end

--[[
@brief 烧写 HexF 文件
1 发送指令 "cpnum ffffffff "
2. 等待对端返回 "#OK>>:"
3. 使用以下流程烧写各个数据段:
	1) 发送段烧写信息指令 "cpbin index flash_addr size run_addr"，其中:
		* index: 格式为 "c0", "c1", ...
		* flash_addr: 是段地址的低 24 位，如段地址是 0x11040000，则 flash_addr = "040000"
		* size: 段数据的长度(十六进制)，如段数据长度为 0xFC18(64536) 字节，则 size = "fc18"
		* run_addr 不需要填写，可以填入段的线性地址，如段地址是 0x11040000，则 run_addr = "11040000"
		* 示例: "cpbin c7 040000 fc18 11040000"
	2) 等待对端返回 "by hex mode:"
	3) 发送段二进制数据(不要有任何多余数据)
	4) 等待对端返回 "checksum is: 0x001ce9b0"
	5) 检验 checksum 正确后，发送 "001ce9b0"
	6) 等待对端返回 "#OK>>:"，表示本段数据烧录成功！
--]]
local function flashHexf(port, hex, keepKVStore)
	local cmd, ack, reason

	-- 擦除 Flash
	eraseFlash(port, keepKVStore);

	-- 发送数据段个数(HexF 烧写时，固定为 "ffffffff")
	-- !!! 指令最后面要多加一个空格表示结束 ?!!!
	ack, reason = cmdAck(port, "cpnum ffffffff ");
	if reason ~= "ok" then error("error: " .. reason); end

	for i, segment in ipairs(hex.segments) do
		-- 发送数据段信息
		local flash_addr = bit.band(segment.flashAddr, 0xFFFFFF);
		cmd = string.format("cpbin c%d %06x %x %08x", i-1, flash_addr, #segment.data, segment.flashAddr);
		ack, reason = cmdAck(port, cmd, nil, "by hex mode:");
		if reason ~= "ok" then error("error: " .. reason); end

		-- 发送数据
		ack, reason = cmdAck(port, segment.data, nil, "checksum is: 0x%x%x%x%x%x%x%x%x", false);
		if reason ~= "ok" then error("error: " .. reason); end
		
		-- 判断 checksum 是否正确
		local ps, pe, checksum = ack:find("checksum is: (0x%x%x%x%x%x%x%x%x)");
		if tonumber(checksum) ~= segment.checksum then error(string.format("checksum error: should be '0x%08x'", segment.checksum)); end
		
		-- 发送 checksum
		cmd = string.format("%08x", segment.checksum);
		ack, reason = cmdAck(port, cmd);
		if reason ~= "ok" then error("error: " .. reason); end
	end
	
	print("Flash successfully !");
end

--[[
@brief 烧写 Hex 文件
1 发送指令 "cpnum ffffffff "
2. 等待对端返回 "#OK>>:"
3. 使用以下流程烧写各个数据段:
	1) 发送段烧写信息指令 "cpbin index flash_addr size run_addr"，其中:
		* index: 格式为 "0", "1", ...
		* flash_addr: 以给定的 Flash 起始地址为基准，每烧写一块，地址向后增加，且要向 4K(0x1000) 对齐
		* size: 段数据的长度(十六进制)，如段数据长度为 0xFC18(64536) 字节，则 size = "fc18"
		* run_addr 就是段的线性地址，如段地址是 0x1FFF4800，则 run_addr = "1FFF4800"
		* 示例: "cpbin 0 9000 B800 1FFF4800"
	2) 等待对端返回 "by hex mode:"
	3) 发送段二进制数据(不要有任何多余数据)
	4) 等待对端返回 "checksum is: 0x001ce9b0"
	5) 检验 checksum 正确后，发送 "001ce9b0"
	6) 等待对端返回 "#OK>>:"，表示本段数据烧录成功！
--]]
local function flashHex(port, hex, flashStartAddr, keepKVStore)
	local cmd, ack, reason
	
	-- 把地址为 1FFF4800 的段放到第一个
	for i = 1, #hex.segments do
		local segment = hex.segments[i];
		if segment.flashAddr == 0x1FFF4800 then
			if i == 1 then break; end
			table.remove(hex.segments, i);
			table.insert(hex.segments, 1, segment);
		end
	end
	
	-- 重新计算 flashAddr 和 runAddr
	print("\r\nResorting segments ...")
	for i, segment in ipairs(hex.segments) do
		-- runAddr 就是段地址
		segment.runAddr = segment.flashAddr;
		-- flashAddr 改为指定的起始地址
		segment.flashAddr = flashStartAddr;
		-- 下一个段的 flashAddr 按顺序向后排，且要向 4K(0x1000) 对齐
		flashStartAddr = flashStartAddr + #segment.data;
		if bit.band(flashStartAddr, 0xFFF) > 0 then 
			flashStartAddr = bit.band(flashStartAddr, 0xFFFFF000) + 0x1000;
		end
		print(string.format("NO.%d   Size: %05X, Flash Address: %5X, Run Address: %08X, checksum: 0x%08x",
			i, #segment.data, segment.flashAddr, segment.runAddr, segment.checksum));
	end

	-- 擦除 Flash
	eraseFlash(port, keepKVStore);

	-- 发送数据段个数
	-- !!! 指令最后面要多加一个空格表示结束 ?!!!
	cmd = string.format("cpnum %d ", #hex.segments);
	ack, reason = cmdAck(port, cmd);
	if reason ~= "ok" then error("error: " .. reason); end

	for i, segment in ipairs(hex.segments) do
		-- 发送数据段信息
		cmd = string.format("cpbin %d %06x %x %08x", i-1, segment.flashAddr, #segment.data, segment.runAddr);
		ack, reason = cmdAck(port, cmd, nil, "by hex mode:");
		if reason ~= "ok" then error("error: " .. reason); end

		-- 发送数据
		ack, reason = cmdAck(port, segment.data, nil, "checksum is: 0x%x%x%x%x%x%x%x%x", false);
		if reason ~= "ok" then error("error: " .. reason); end
		
		-- 判断 checksum 是否正确
		local ps, pe, checksum = ack:find("checksum is: (0x%x%x%x%x%x%x%x%x)");
		if tonumber(checksum) ~= segment.checksum then error(string.format("checksum error: should be '0x%08x'", segment.checksum)); end
		
		-- 发送 checksum
		cmd = string.format("%08x", segment.checksum);
		ack, reason = cmdAck(port, cmd);
		if reason ~= "ok" then error("error: " .. reason); end
	end
	
	print("Flash successfully !");
end

-- 从指定地址读取一个32位整数
-- @addr 要读取的地址, 如 0x11004000
-- @return u32 读取到的32位整数
local function flashRead32(port, addr)
	local cmd, ack, reason

	-- 读取
	cmd = string.format("rdreg%08X ", addr);
	ack, reason = cmdAck(port, cmd, nil, "=0x%x%x%x%x%x%x%x%x#OK>>:", false);
	if reason ~= "ok" then error("error: " .. reason); end
	-- 解析返回值
	local ps, pe, val32 = ack:find("=(0x%x%x%x%x%x%x%x%x)");
	return tonumber(val32);
end

-- 写一个32位整数到指定地址
-- @addr 要写入的地址, 如 0x11004000
-- @u32 32位整数，如 0x15502203
-- @verify 是否读取并校验写入正确性, 默认 true
local function flashWrite32(port, addr, u32, verify)
	verify = verify or true;
	local cmd, ack, reason
	
	-- 写入
	cmd = string.format("write%08X %08X ", addr, u32);
	ack, reason = cmdAck(port, cmd);
	if reason ~= "ok" then error("error: " .. reason); end
	
	-- 不校验就直接返回
	if not verify then return; end

	-- 读取
	local val32 = flashRead32(port, addr);
	if tonumber(val32) ~= u32 then error(string.format("Write verfiy error: should be '0x%08x'", u32)); end
end

-- 烧写 MAC 地址
-- @param port 串口对象
-- @param mac MAC 地址(二进制字符串, 6 字节, 大端在前)
local function flashMac(port, mac)
	if type(mac) ~= "string" or #mac ~= 6 then error("Invalid MAC address !"); end
	local cmd, ack, reason, pos, hmac, lmac

	-- 转换 MAC 为整数
	pos, hmac = mac:unpack("<H", 1);   -- 高2字节(转换为小端模式)
	pos, lmac = mac:unpack("<I", pos); -- 低4字节(转换为小端模式)
	
	-- 发送 ufifo3
	ack, reason = cmdAck(port, "ufifo3 ");
	if reason ~= "ok" then error("error: " .. reason); end
	
	-- 写低4字节到 0x11004000
	flashWrite32(port, 0x11004000, lmac);
	-- 写高2字节到 0x11004004
	flashWrite32(port, 0x11004004, hmac);
end

return {
	checksum = zylib.sum8;
	open = open;
	resetToFlash = resetToFlash;
	resetToRun = resetToRun;
	cmdAck = cmdAck;
	flashHexf = flashHexf;
	flashHex = flashHex;
	flashRead32 = flashRead32;
	flashWrite32 = flashWrite32;
	flashMac = flashMac;
}
